package jvn

import (
	"encoding/xml"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"runtime"
	"strings"
	"time"

	"github.com/PuerkitoBio/goquery"
	"github.com/spf13/viper"
	"github.com/vulsio/go-cve-dictionary/fetcher"
	"github.com/vulsio/go-cve-dictionary/log"
	"github.com/vulsio/go-cve-dictionary/models"
	"github.com/vulsio/go-cve-dictionary/util"
	"golang.org/x/xerrors"
)

// Meta ... https://jvndb.jvn.jp/ja/feed/checksum.txt
type Meta struct {
	URL          string `json:"url"`
	Hash         string `json:"sha256"`
	LastModified string `json:"lastModified"`
}

type rdf struct {
	Items []Item `xml:"item"`
}

// Item ... http://jvndb.jvn.jp/apis/getVulnOverviewList_api.html
type Item struct {
	About       string       `xml:"about,attr"`
	Title       string       `xml:"title"`
	Link        string       `xml:"link"`
	Description string       `xml:"description"`
	Publisher   string       `xml:"publisher"`
	Identifier  string       `xml:"identifier"`
	References  []references `xml:"references"`
	Cpes        []cpe        `xml:"cpe"`
	Cvsses      []Cvss       `xml:"cvss"`
	Date        string       `xml:"date"`
	Issued      string       `xml:"issued"`
	Modified    string       `xml:"modified"`
}

type cpe struct {
	Version string `xml:"version,attr"` // cpe:/a:mysql:mysql
	Vendor  string `xml:"vendor,attr"`
	Product string `xml:"product,attr"`
	Value   string `xml:",chardata"`
}

type references struct {
	ID     string `xml:"id,attr"`
	Source string `xml:"source,attr"`
	Title  string `xml:"title,attr"`
	URL    string `xml:",chardata"`
}

// Cvss ... CVSS
type Cvss struct {
	Score    string `xml:"score,attr"`
	Severity string `xml:"severity,attr"`
	Vector   string `xml:"vector,attr"`
	Version  string `xml:"version,attr"`
}

// FetchConvert Fetch CVE vulnerability information from JVN
func FetchConvert(uniqCve map[string]map[string]models.Jvn, years []string) error {
	for _, y := range years {
		items, err := fetch(y)
		if err != nil {
			return xerrors.Errorf("Failed to fetch. err: %w", err)
		}

		cves, err := convert(items)
		if err != nil {
			return xerrors.Errorf("Failed to convert. err: %w", err)
		}
		distributeCvesByYear(uniqCve, cves)
	}
	return nil
}

func fetch(year string) ([]Item, error) {
	url, err := models.GetURLByYear(models.JvnType, year)
	if err != nil {
		return nil, xerrors.Errorf("Failed to GetURLByYear. err: %w", err)
	}

	body, err := fetcher.FetchFeedFile(url, false)
	if err != nil {
		return nil, xerrors.Errorf("Failed to FetchFeedFiles. err: %w", err)
	}

	var rdf rdf
	items := []Item{}
	if err := xml.Unmarshal([]byte(body), &rdf); err != nil {
		return nil, xerrors.Errorf("Failed to unmarshal. url: %s, err: %w", url, err)
	}
	for i, item := range rdf.Items {
		if !(strings.Contains(item.Description, "** 未確定 **") || strings.Contains(item.Description, "** サポート外 **") || strings.Contains(item.Description, "** 削除 **")) {
			items = append(items, rdf.Items[i])
		}
	}

	return items, nil
}

func convert(items []Item) (map[string]models.Jvn, error) {
	reqChan := make(chan Item, len(items))
	resChan := make(chan []models.Jvn, len(items))
	errChan := make(chan error)
	defer close(reqChan)
	defer close(resChan)
	defer close(errChan)

	go func() {
		for _, item := range items {
			reqChan <- item
		}
	}()

	concurrency := runtime.NumCPU() + 2
	tasks := util.GenWorkers(concurrency)
	for range items {
		tasks <- func() {
			req := <-reqChan
			cves, err := convertToModel(&req)
			if err != nil {
				errChan <- err
				return
			}
			resChan <- cves
		}
	}

	cves := map[string]models.Jvn{}
	errs := []error{}
	timeout := time.After(10 * 60 * time.Second)
	for range items {
		select {
		case res := <-resChan:
			for _, cve := range res {
				uniqJVNID := fmt.Sprintf("%s#%s", cve.JvnID, cve.CveID)
				cves[uniqJVNID] = cve
			}
		case err := <-errChan:
			errs = append(errs, err)
		case <-timeout:
			return nil, fmt.Errorf("Timeout Fetching")
		}
	}
	if 0 < len(errs) {
		return nil, xerrors.Errorf("%w", errs)
	}
	return cves, nil
}

// convertJvn converts Jvn structure(got from JVN) to model structure.
func convertToModel(item *Item) (cves []models.Jvn, err error) {
	var cvss2, cvss3 Cvss
	for _, cvss := range item.Cvsses {
		if strings.HasPrefix(cvss.Version, "2") {
			cvss2 = cvss
		} else if strings.HasPrefix(cvss.Version, "3") {
			cvss3 = cvss
		} else {
			log.Warnf("Unknown CVSS version format: %s", cvss.Version)
		}
	}

	//  References
	refs, links := []models.JvnReference{}, []string{}
	for _, r := range item.References {
		ref := models.JvnReference{
			Reference: models.Reference{
				Link:   r.URL,
				Name:   r.Title,
				Source: r.Source,
			},
		}
		refs = append(refs, ref)

		if ref.Source == "JPCERT-AT" {
			links = append(links, r.URL)
		}
	}

	certs, err := collectCertLinks(links)
	if err != nil {
		return nil,
			xerrors.Errorf("Failed to collect links. err: %w", err)
	}

	// Cpes
	cpes := []models.JvnCpe{}
	for _, c := range item.Cpes {
		cpeBase, err := fetcher.ParseCpeURI(c.Value)
		if err != nil {
			// logging only
			log.Warnf("Failed to parse CPE URI: %s, %s", c.Value, err)
			continue
		}
		cpes = append(cpes, models.JvnCpe{
			CpeBase: *cpeBase,
		})
	}

	publish, err := parseJvnTime(item.Issued)
	if err != nil {
		return nil, err
	}
	modified, err := parseJvnTime(item.Modified)
	if err != nil {
		return nil, err
	}

	cveIDs := getCveIDs(*item)
	if len(cveIDs) == 0 {
		log.Debugf("No CveIDs in references. JvnID: %s, Link: %s",
			item.Identifier, item.Link)
		// ignore this item
		return nil, nil
	}

	for _, cveID := range cveIDs {
		v2elems := parseCvss2VectorStr(cvss2.Vector)
		v3elems := parseCvss3VectorStr(cvss3.Vector)
		cve := models.Jvn{
			CveID:   cveID,
			Title:   strings.Replace(item.Title, "\r", "", -1),
			Summary: strings.Replace(item.Description, "\r", "", -1),
			JvnLink: item.Link,
			JvnID:   item.Identifier,

			Cvss2: models.JvnCvss2{
				Cvss2: models.Cvss2{
					BaseScore:             fetcher.StringToFloat(cvss2.Score),
					Severity:              cvss2.Severity,
					VectorString:          cvss2.Vector,
					AccessVector:          v2elems[0],
					AccessComplexity:      v2elems[1],
					Authentication:        v2elems[2],
					ConfidentialityImpact: v2elems[3],
					IntegrityImpact:       v2elems[4],
					AvailabilityImpact:    v2elems[5],
				},
			},

			Cvss3: models.JvnCvss3{
				Cvss3: models.Cvss3{
					BaseScore:             fetcher.StringToFloat(cvss3.Score),
					BaseSeverity:          cvss3.Severity,
					VectorString:          cvss3.Vector,
					AttackVector:          v3elems[0],
					AttackComplexity:      v3elems[1],
					PrivilegesRequired:    v3elems[2],
					UserInteraction:       v3elems[3],
					Scope:                 v3elems[4],
					ConfidentialityImpact: v3elems[5],
					IntegrityImpact:       v3elems[6],
					AvailabilityImpact:    v3elems[7],
				},
			},

			References: append([]models.JvnReference{}, refs...),
			Cpes:       append([]models.JvnCpe{}, cpes...),
			Certs:      append([]models.JvnCert{}, certs...),

			PublishedDate:    publish,
			LastModifiedDate: modified,
		}
		cves = append(cves, cve)
	}
	return
}

func collectCertLinks(links []string) (certs []models.JvnCert, err error) {
	var proxyURL *url.URL
	httpClient := &http.Client{}
	if viper.GetString("http-proxy") != "" {
		if proxyURL, err = url.Parse(viper.GetString("http-proxy")); err != nil {
			return nil, xerrors.Errorf("failed to parse proxy url: %w", err)
		}
		httpClient = &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
	}

	certs = []models.JvnCert{}
	for _, link := range links {
		title := ""
		if strings.HasSuffix(link, ".html") {
			res, err := httpClient.Get(link)
			if err != nil {
				return nil, xerrors.Errorf("Failed to get %s: err: %w", link, err)
			}
			defer res.Body.Close()

			doc, err := goquery.NewDocumentFromReader(res.Body)
			if err != nil {
				return nil, xerrors.Errorf("Failed to NewDocumentFromReader. err: %w", err)
			}
			title = doc.Find("title").Text()
		}
		certs = append(certs, models.JvnCert{
			Cert: models.Cert{
				Title: title,
				Link:  link,
			},
		})
	}

	return certs, nil
}

var cvss2VectorMap = map[string]string{
	"AV:L": "LOCAL",
	"AV:A": "ADJACENT_NETWORK",
	"AV:N": "NETWORK",

	"AC:L": "LOW",
	"AC:M": "MEDIUM",
	"AC:H": "HIGH",

	"Au:M": "MULTIPLE",
	"Au:S": "SINGLE",
	"Au:N": "NONE",

	"C:N": "NONE",
	"C:P": "PARTIAL",
	"C:C": "COMPLETE",

	"I:N": "NONE",
	"I:P": "PARTIAL",
	"I:C": "COMPLETE",

	"A:N": "NONE",
	"A:P": "PARTIAL",
	"A:C": "COMPLETE",
}

func parseCvss2VectorStr(str string) (elems []string) {
	if len(str) == 0 {
		return []string{"", "", "", "", "", ""}
	}
	for _, s := range strings.Split(str, "/") {
		elems = append(elems, cvss2VectorMap[s])
	}
	return
}

var cvss3VectorMap = map[string]string{
	"AV:N": "NETWORK",
	"AV:A": "ADJACENT_NETWORK",
	"AV:L": "LOCAL",
	"AV:P": "PHYSICAL",

	"AC:L": "LOW",
	"AC:H": "HIGH",

	"PR:N": "NONE",
	"PR:L": "LOW",
	"PR:H": "HIGH",

	"UI:N": "NONE",
	"UI:R": "REQUIRED",

	"S:U": "UNCHANGED",
	"S:C": "CHANGED",

	"C:N": "NONE",
	"C:L": "LOW",
	"C:H": "HIGH",

	"I:N": "NONE",
	"I:L": "LOW",
	"I:H": "HIGH",

	"A:N": "NONE",
	"A:L": "LOW",
	"A:H": "HIGH",
}

func parseCvss3VectorStr(str string) (elems []string) {
	if len(str) == 0 {
		return []string{"", "", "", "", "", "", "", ""}
	}
	str = strings.TrimPrefix(str, "CVSS:3.0/")
	for _, s := range strings.Split(str, "/") {
		elems = append(elems, cvss3VectorMap[s])
	}
	return
}

// convert string time to time.Time
// JVN : "2016-01-26T13:36:23+09:00",
// NVD : "2016-01-20T21:59:01.313-05:00",
func parseJvnTime(strtime string) (t time.Time, err error) {
	layout := "2006-01-02T15:04-07:00"
	t, err = time.Parse(layout, strtime)
	if err != nil {
		return t, xerrors.Errorf("Failed to parse time, time: %s, err: %w", strtime, err)
	}
	return
}

var cveIDPattern = regexp.MustCompile(`^CVE-[0-9]{4}-[0-9]{4,}$`)

func getCveIDs(item Item) []string {
	cveIDsMap := map[string]bool{}
	for _, ref := range item.References {
		switch ref.Source {
		case "NVD", "CVE":
			if cveIDPattern.MatchString(ref.ID) {
				cveIDsMap[ref.ID] = true
			} else {
				id := strings.TrimSpace(ref.ID)
				if cveIDPattern.MatchString(id) {
					log.Warnf("CVE-ID with extra space. Please email JVNDB (isec-jvndb@ipa.go.jp) to fix the rdf file with the following information. RDF data(Identifier: %s, Reference Source: %s, ID: %s)", item.Identifier, ref.Source, ref.ID)
					cveIDsMap[id] = true
				} else {
					log.Warnf("Failed to get CVE-ID. Invalid CVE-ID. Please email JVNDB (isec-jvndb@ipa.go.jp) to fix the rdf file with the following information. RDF data(Identifier: %s, Reference Source: %s, ID: %s)", item.Identifier, ref.Source, ref.ID)
				}
			}
		}
	}
	cveIDs := []string{}
	for cveID := range cveIDsMap {
		cveIDs = append(cveIDs, cveID)
	}
	return cveIDs
}

func distributeCvesByYear(uniqCves map[string]map[string]models.Jvn, cves map[string]models.Jvn) {
	for uniqJVNID, cve := range cves {
		y := strings.Split(cve.JvnID, "-")[1]
		if _, ok := uniqCves[y]; !ok {
			uniqCves[y] = map[string]models.Jvn{}
		}
		if destCve, ok := uniqCves[y][uniqJVNID]; !ok {
			uniqCves[y][uniqJVNID] = cve
		} else {
			if cve.LastModifiedDate.After(destCve.LastModifiedDate) {
				uniqCves[y][uniqJVNID] = cve
			}
		}
	}
}
